package com.jf.nep.render;

import java.nio.FloatBuffer;
import java.util.List;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.util.vector.Matrix4f;
import org.lwjgl.util.vector.Vector3f;
import org.lwjgl.util.vector.Vector4f;

import com.jf.nep.MathUtil;
import com.jf.nep.shader.ShaderProgram;
import com.jf.nep.shader.ShaderStore;

/**
 * Renderer functionality on a mesh level. Supports dynamic adding and removal of meshes.
 * Caller is responsible for high level culling/scene handling.  
 */
public class MeshRenderer {    
    private int width;
    private int height;
    private boolean useFullScreen;
    private MeshStoreImpl meshStore = new MeshStoreImpl();
    private ShaderStore shaderStore;
    private Matrix4f viewMatrix;
    private Vector3f cameraAngle;
    private Vector3f cameraPos;
    private Matrix4f projectionMatrix;
    private FloatBuffer lightBuffer;
    private Vector4f lightVector;
    private Vector4f rotatedLightVector;
    private Matrix4f lightMatrix;
    private FloatBuffer colorBuffer;
    private FloatBuffer projectionMatrixBuffer;
    private FloatBuffer viewMatrixBuffer;
    private FloatBuffer modelMatrixBuffer;
    
    public MeshRenderer(boolean useFullScreen) {
        this.useFullScreen = useFullScreen;
    }
    
    /** Create the renderer context */
    public void create() throws Exception {
        initGl();
        initMatrices();
        
        cameraPos = new Vector3f(0,-0.9f,0);
        cameraAngle = new Vector3f(0,0f,0);
        
        setLightAngle(new Vector3f(0,0,0));
    }
    

    /** Set new angle of light */
    public void setLightAngle(Vector3f lightAngle) {
        lightMatrix.setIdentity();
        Matrix4f.rotate(MathUtil.degreesToRadians(lightAngle.z), new Vector3f(0,0,1), lightMatrix, lightMatrix);
        Matrix4f.rotate(MathUtil.degreesToRadians(lightAngle.y), new Vector3f(0,1,0), lightMatrix, lightMatrix);
        Matrix4f.rotate(MathUtil.degreesToRadians(lightAngle.x), new Vector3f(1,0,0), lightMatrix, lightMatrix);
        
        Matrix4f.transform(lightMatrix, lightVector, rotatedLightVector);
        rotatedLightVector.normalise();
        
//        System.out.println("Light vector = " + rotatedLightVector.toString());
        //rotatedLightVector.set(0,1,0,1);
        rotatedLightVector.store(lightBuffer);
        lightBuffer.flip();
    }
    
    public void setShaderStore(ShaderStore shaderStore) {
        this.shaderStore = shaderStore; 
    }

    /** Get the mesh store managing mesh data allocated in graphics card memory */ 
    public MeshStore getMeshStore() {
        return meshStore;
    }

    /**
     * Let the renderer process a list of mesh instances. The meshes references by the instances must already 
     * been downloaded by createMesh() or they will be ignored. 
     */
    public boolean process(List<MeshInstance> meshes) throws Exception {
        if (Display.isCloseRequested()) {
            return false;
        }
        GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
        setupCamera();
        downloadMatrixBuffers(meshes);        
        render(meshes);
        Display.update();
        
        return true;
        
    }        

            
    public void destroy() {
        Display.destroy();
    }
    
    public void setCameraAngle(Vector3f cameraAngle) {
        this.cameraAngle = cameraAngle;
    }
    
    public void setCameraPos(Vector3f cameraPos) {
        this.cameraPos = cameraPos;
    }
    
    public Vector3f getCameraAngle() {
        return cameraAngle;
    }
    
    public Vector3f getCameraPos() {
        return cameraPos;
    }
    
    
    private void initMatrices() {
        projectionMatrix = new Matrix4f();
        viewMatrix = new Matrix4f();
        lightMatrix = new Matrix4f(); 
        lightVector = new Vector4f(0,0,-1,1);
        rotatedLightVector = new Vector4f();
        
        float fieldOfView = 65f;
        float aspectRatio = (float)width/(float)height;
        float nearPlane = 0.1f;
        float farPlane = 100f;
        
        float yScale = MathUtil.coTangent(MathUtil.degreesToRadians(fieldOfView / 2f));
        float xScale = yScale / aspectRatio;        
        float frustrumLength = farPlane - nearPlane;
        
        projectionMatrix.m00 = xScale;
        projectionMatrix.m11 = yScale;
        projectionMatrix.m22 = -((farPlane + nearPlane) / frustrumLength);
        projectionMatrix.m23 = -1;
        projectionMatrix.m32 = -((2 * nearPlane * farPlane) / frustrumLength);
        
        projectionMatrixBuffer = BufferUtils.createFloatBuffer(16);
        projectionMatrix.store(projectionMatrixBuffer); 
        projectionMatrixBuffer.flip();
        
        
        viewMatrixBuffer = BufferUtils.createFloatBuffer(16);
        modelMatrixBuffer = BufferUtils.createFloatBuffer(16);        
        lightBuffer = BufferUtils.createFloatBuffer(4);
        colorBuffer = BufferUtils.createFloatBuffer(4);
        new Vector4f(0.6f,0.3f,0.2f,1).store(colorBuffer);
        colorBuffer.flip();
    }
    
            
    private void initGl() throws Exception {
        // Setup an OpenGL context with API version 3.2
        try {
            PixelFormat pixelFormat = new PixelFormat();
            ContextAttribs contextAtrributes = new ContextAttribs(3, 2).withProfileCore(true);
            contextAtrributes.withForwardCompatible(true);
            contextAtrributes.withProfileCore(true);

            if (useFullScreen) {
                Display.setFullscreen(true);
                Display.setVSyncEnabled(true);
            } else {
                DisplayMode desktopMode = Display.getDesktopDisplayMode();
                Display.setDisplayMode(new DisplayMode(desktopMode.getWidth() * 4 / 5, desktopMode.getHeight() * 4 / 5));
            }
            Display.setTitle("Awesome Game");
            Display.create(pixelFormat, contextAtrributes);
            
            width = Display.getWidth();
            height = Display.getHeight();
            GL11.glViewport(0, 0, width, height);
            
        } catch (LWJGLException e) {
            throw new Exception(e);
        }
        System.out.println("OpenGL version: " + GL11.glGetString(GL11.GL_VERSION));
               
        GL11.glClearColor(0.2f,0.2f,0.2f, 0f);
        GL11.glEnable(GL11.GL_DEPTH_TEST);       
    }

    private void setupCamera() {
        viewMatrix.setIdentity();
        //viewMatrix = new Matrix4f();       
        Matrix4f.rotate(MathUtil.degreesToRadians(cameraAngle.z), new Vector3f(0,0,1), viewMatrix, viewMatrix);
        Matrix4f.rotate(MathUtil.degreesToRadians(cameraAngle.x), new Vector3f(1,0,0), viewMatrix, viewMatrix);
        Matrix4f.rotate(MathUtil.degreesToRadians(cameraAngle.y), new Vector3f(0,1,0), viewMatrix, viewMatrix);
        Matrix4f.translate(cameraPos, viewMatrix, viewMatrix);
        
        viewMatrix.store(viewMatrixBuffer);
        viewMatrixBuffer.flip();
    }
    
    private void downloadMatrixBuffers(List<MeshInstance> meshes) {
        for (ShaderProgram program : shaderStore.getShaderPrograms()) {
            //System.out.println("Use program = " + program.getProgramId());
            GL20.glUseProgram(program.getProgramId());        
            GL20.glUniformMatrix4(program.getProjectionMatrixLocation(), false, projectionMatrixBuffer);
            GL20.glUniformMatrix4(program.getViewMatrixLocation(), false, viewMatrixBuffer);
            GL20.glUniform4(program.getLightVectorLocation() , lightBuffer);
            GL20.glUniform4(program.getColorVectorLocation(), colorBuffer);
        }
        
        for (MeshInstance mesh : meshes) {
            MeshAllocation allocation = meshStore.getAllocation(mesh.getMeshId());
            if (allocation == null) {
                System.err.println("Mesh not created: " + mesh.getMeshId());
                continue;
            }
            ShaderProgram shaderProgram = shaderStore.getShaderProgram(mesh.getProgramId());
            if (shaderProgram == null) {
                System.err.println("Shader program is null for mesh id " + mesh.getMeshId());
                continue;
            }
            //System.out.println("Use mesh = " + mesh.getMeshId() + " and program " + mesh.getProgramId());
            GL20.glUseProgram(shaderProgram.getProgramId());        
            
            mesh.getModelMatrix().store(modelMatrixBuffer);
            modelMatrixBuffer.flip();
            GL20.glUniformMatrix4(shaderProgram.getModelMatrixLocation(), false, modelMatrixBuffer);
            
            mesh.getRotationMatrix().store(modelMatrixBuffer);
            modelMatrixBuffer.flip();
            GL20.glUniformMatrix4(shaderProgram.getNormalMatrixLocation(), false, modelMatrixBuffer);
        }
        
        GL20.glUseProgram(0);
        
    }

    private void render(List<MeshInstance> meshes) {
        for (MeshInstance instance : meshes) {
            MeshAllocation allocation = meshStore.getAllocation(instance.getMeshId());
            if (allocation == null) {
                continue;
            }
            GL20.glUseProgram(shaderStore.getShaderProgram(instance.getProgramId()).getProgramId());
            
            // Bind the texture
            //GL13.glActiveTexture(GL13.GL_TEXTURE0);
            //GL11.glBindTexture(GL11.GL_TEXTURE_2D, allocation.mesh.textureId);
            
            // Bind to the VAO that has all the information about the vertices
            GL30.glBindVertexArray(allocation.vaoId);
            for (int i = 0; i < Vertex.NUMBER_OF_ELEMENT_TYPES; i++) {
                GL20.glEnableVertexAttribArray(i);
            }
            
            // Bind to the index VBO that has all the information about the order of the vertices
            GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, allocation.vboiId);

            // Draw the vertices
            GL11.glDrawElements(GL11.GL_TRIANGLES, allocation.indicesCount, GL11.GL_UNSIGNED_BYTE, 0);

            // Put everything back to default (deselect)
            GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
            
            for (int i = 0; i < Vertex.NUMBER_OF_ELEMENT_TYPES; i++) {
                GL20.glDisableVertexAttribArray(i);
            }
            
            GL30.glBindVertexArray(0);
            GL20.glUseProgram(0);
        }
    }

}
